package ru.lukyanets.transmission.protocol.client.builder;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.apache.hc.core5.http.HttpStatus;
import ru.lukyanets.transmission.protocol.client.QueueAction;
import ru.lukyanets.transmission.protocol.client.RpcClient;
import ru.lukyanets.transmission.protocol.client.TorrentAction;
import ru.lukyanets.transmission.protocol.client.TorrentFileMarker;
import ru.lukyanets.transmission.protocol.client.exception.RequestException;
import ru.lukyanets.transmission.protocol.client.exception.TransmissionException;
import ru.lukyanets.transmission.protocol.client.exception.WebClientException;
import ru.lukyanets.transmission.protocol.model.Method;
import ru.lukyanets.transmission.protocol.model.TRequest;
import ru.lukyanets.transmission.protocol.model.TResponse;
import ru.lukyanets.transmission.protocol.model.session.Session;
import ru.lukyanets.transmission.protocol.model.session.stats.SessionStats;
import ru.lukyanets.transmission.protocol.model.torrent.Torrent;
import ru.lukyanets.transmission.protocol.model.torrent.TorrentFields;
import ru.lukyanets.transmission.protocol.model.torrent.Torrents;
import ru.lukyanets.transmission.protocol.model.torrent.arguments.TorrentActionArgs;
import ru.lukyanets.transmission.protocol.model.torrent.arguments.TorrentAddArgs;
import ru.lukyanets.transmission.protocol.model.torrent.arguments.TorrentGetArgs;
import ru.lukyanets.transmission.protocol.model.torrent.arguments.TorrentRemoveArgs;
import ru.lukyanets.transmission.protocol.model.torrent.arguments.TorrentRenamePathArgs;
import ru.lukyanets.transmission.protocol.model.torrent.arguments.TorrentSetArgs;
import ru.lukyanets.transmission.protocol.model.torrent.arguments.TorrentSetLocArgs;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

/**
 * Transmission RPC Client based on OkHttp REST client
 *
 * @author slukyanets
 */
public final class RestRpcClient implements RpcClient {
    private static final String SESSION_HEADER_NAME = "X-Transmission-Session-Id";
    private static final String SUCCESS = "success";
    private static final MediaType MediaTypeJSON = MediaType
            .parse("application/json; charset=utf-8");

    private final OkHttpClient client;
    private final String targetUrl;
    private String sessionId;
    private final ObjectMapper mapper = new ObjectMapper();

    private final int maxAttempts;

    /**
     * @param client      - constructed by builder rest client.
     * @param targetUrl   - url to the server.
     * @param maxAttempts - maximum retries.
     */
    RestRpcClient(OkHttpClient client, String targetUrl, int maxAttempts) {
        this.client = client;
        this.targetUrl = targetUrl;

        this.maxAttempts = maxAttempts > 1 ? maxAttempts : 2;
    }

    /**
     * Make low level request to server with OkHttp.
     *
     * @param entity - Object to be serialized to JSON.
     * @return OkHttp Response object.
     */
    private Response makeHttpRequest(TRequest<?> entity) throws WebClientException {
        Response response;
        int i = 0;
        while (i < maxAttempts) {
            i++;
            Request request;
            try {
                RequestBody body = RequestBody.create(mapper.writeValueAsString(entity), MediaTypeJSON);
                Request.Builder requestBuilder = new Request.Builder()
                        .url(targetUrl)
                        .post(body);
                if (sessionId != null) {
                    requestBuilder.header(SESSION_HEADER_NAME, sessionId);
                }
                request = requestBuilder.build();
            } catch (JsonProcessingException e) {
                throw new RuntimeException("Error while serializing entity", e);
            }

            try {
                response = client.newCall(request).execute();
            } catch (IOException e) {
                throw new WebClientException(e);
            }

            if (response.code() == HttpStatus.SC_CONFLICT) {
                obtainSession(response);
            } else if (response.code() == HttpStatus.SC_REQUEST_TIMEOUT) {
                throw new WebClientException(response.message());
            } else if (response.code() == HttpStatus.SC_OK) {
                return response;
            }
        }
        throw new WebClientException("Exceeded Max Connection Attempts");
    }

    /**
     * Extracts transmission session ID from response header.
     *
     * @param response - OkHttp {@link Response} object.
     */
    private void obtainSession(Response response) {
        this.sessionId = response.header(SESSION_HEADER_NAME);
        if (response.body() != null) {
            response.body().close();
        }
    }

    /**
     * Generic method to make high level request with some arguments.
     *
     * @param arguments - Arguments Object to be serialized to JSON. e.g. {@link ru.lukyanets.transmission.protocol.model.torrent.arguments.TorrentGetArgs}
     * @param method    - instance of {@link Method} object
     * @return entity.
     */
    private <T, U> U makeRequest(T arguments, Method<U> method) throws TransmissionException {

        TRequest<T> request = new TRequest<>();
        request.setArguments(arguments);
        request.setMethod(method.getMethod());

        TResponse<U> tResponse;
        Response response = makeHttpRequest(request);
        try {
            tResponse = mapper.readValue(
                    Optional.ofNullable(response.body()).map(ResponseBody::charStream).orElse(null),
                    method.getResponseType());
        } catch (IOException e) {
            throw new RuntimeException("Error while deserialize message", e);
        } finally {
            if (response.body() != null) {
                response.body().close();
            }
        }

        if (tResponse.getResult().equals(SUCCESS)) {
            return tResponse.getArguments();
        } else {
            throw new RequestException(tResponse.getResult());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Torrent> getTorrents() throws TransmissionException {
        TorrentGetArgs arguments = new TorrentGetArgs();
        arguments.setFields(TorrentFields.TORRENTS_LIST_FIELDS);

        Torrents torrents = makeRequest(arguments, Method.TORRENT_GET);

        return torrents.getTorrents();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Torrent getTorrent(int id) throws TransmissionException {
        TorrentGetArgs arguments = new TorrentGetArgs();
        arguments.setFields(new ArrayList<>(TorrentFields.TORRENTS_LIST_FIELDS));
        arguments.getFields().addAll(TorrentFields.TORRENT_DETAILS_FIELDS);
        arguments.setIds(Collections.singletonList(id));

        List<Torrent> torrents = makeRequest(arguments, Method.TORRENT_GET).getTorrents();
        if (torrents.size() > 0) {
            return torrents.get(0);
        } else {
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setTorrent(TorrentSetArgs arguments) throws TransmissionException {

        makeRequest(arguments, Method.TORRENT_SET);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void actionTorrent(List<Integer> ids, TorrentAction action) throws TransmissionException {
        TorrentActionArgs arguments = new TorrentActionArgs();
        arguments.setIds(ids);
        makeRequest(arguments, action.getMethod());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int addTorrent(String metainfo, List<TorrentFileMarker> files, String cookies,
                          String downloadDir, boolean paused, int peerLimit, int bandwidthPriority) throws TransmissionException {
        TorrentAddArgs arguments = new TorrentAddArgs();
        arguments.setMetainfo(metainfo);
        arguments.setCookies(cookies);
        arguments.setDownloadDir(downloadDir);
        arguments.setPaused(paused);
        arguments.setPeerLimit(peerLimit);
        arguments.setBandwidthPriority(bandwidthPriority);

        List<Integer> filesWanted = new ArrayList<>();
        List<Integer> filesUnwanted = new ArrayList<>();
        List<Integer> priorityHigh = new ArrayList<>();
        List<Integer> priorityLow = new ArrayList<>();
        List<Integer> priorityNormal = new ArrayList<>();
        if (files != null) {
            for (TorrentFileMarker file : files) {
                if (file.isWanted()) {
                    filesWanted.add(file.getId());
                } else {
                    filesUnwanted.add(file.getId());
                }

                switch (file.getPriority()) {
                    case HIGH -> priorityHigh.add(file.getId());
                    case LOW -> priorityLow.add(file.getId());
                    default -> priorityNormal.add(file.getId());
                }
            }
        }

        arguments.setFilesWanted(filesWanted);
        arguments.setFilesUnwanted(filesUnwanted);
        arguments.setPriorityHigh(priorityHigh);
        arguments.setPriorityLow(priorityLow);
        arguments.setPriorityNormal(priorityNormal);

        return makeRequest(arguments, Method.TORRENT_ADD).getTorrentAdded().getId();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeTorrent(List<Integer> ids, boolean deleteLocalData) throws TransmissionException {
        TorrentRemoveArgs arguments = new TorrentRemoveArgs();
        arguments.setIds(ids);
        arguments.setDeleteLocalData(deleteLocalData);

        makeRequest(arguments, Method.TORRENT_REMOVE);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setLocationTorrent(List<Integer> ids, String location, boolean move) throws TransmissionException {
        TorrentSetLocArgs args = new TorrentSetLocArgs();
        args.setIds(ids);
        args.setLocation(location);
        args.setMove(move);

        makeRequest(args, Method.TORRENT_SET_LOCATION);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void renamePathTorrent(List<Integer> ids, String path, String name) throws TransmissionException {
        TorrentRenamePathArgs args = new TorrentRenamePathArgs();
        args.setIds(ids);
        args.setPath(path);
        args.setName(name);

        makeRequest(args, Method.TORRENT_RENAME_PATH);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void actionQueue(List<Integer> ids, QueueAction action) throws TransmissionException {
        TorrentActionArgs args = new TorrentRemoveArgs();
        args.setIds(ids);
        makeRequest(args, action.getMethod());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean testPort() throws TransmissionException {
        return makeRequest(null, Method.PORT_TEST).getPortIsOpen();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Session getSession() throws TransmissionException {

        return makeRequest(null, Method.SESSION_GET);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setSession(Session session) throws TransmissionException {
        makeRequest(session, Method.SESSION_SET);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void shutdownServer() throws TransmissionException {
        makeRequest(null, Method.SESSION_CLOSE);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SessionStats getSessionStats() throws TransmissionException {
        return makeRequest(null, Method.SESSION_STATS);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void updateBlocklist() throws TransmissionException {
        makeRequest(null, Method.BLOCKLIST_UPDATE);
    }
}
